/*
 * Copyright (c) 2011-2012 Alexander Dubu
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * o  Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * o  Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * o  Neither the name Axil nor the names of its contributors may be used to
 *    endorse or promote products derived from this software without specific
 *    prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

package axil.engine;

import axil.api.*;
import axil.api.access.FieldDefinition;
import axil.api.error.AxilCompilationException;
import axil.api.extend.Environment;
import axil.api.extend.SourceAdapter;
import axil.compiler.AxilCompiler;
import axil.engine.adapter.StandardSourceAdapter;
import axil.framework.format.FormatBuilder;
import axil.framework.format.MaskFormatting;
import axil.framework.localization.Locality;
import axil.framework.localization.Namespace;
import axil.framework.user.User;
import axil.stdlib.core.type.Nil;
import axil.vm.Env;
import axil.vm.Executable;

import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import static axil.framework.Functions.*;


/**
 * The actual implementation of the root Axil native API. From here all things
 * spring.
 */
public class AxilEngine extends Axil {
	public static final Namespace ns = Namespace.root.ns("axil");
	private static final AxilEngine instance = new AxilEngine();
	private static final String placeholder = "(placeholder)";

	private Case sensitivity;
	private Transformation transform;
	private Config config;
	private List<SourceAdapter> sources;
	private boolean sealed;


	private AxilEngine() {
		this.sensitivity = Case.SENSITIVE;
		this.transform = Transformation.DASHES;
		this.config = new Config("data/compiler.cfg");
		this.sources = new ArrayList<SourceAdapter>();

		sources.add(new StandardSourceAdapter());
		this.sealed = false;
	}


	public static AxilEngine instance() {
		return instance;
	}


	/**
	 * Get the exact version and release information for the underlying runtime
	 * engine.
	 *
	 * @return
	 * 	Returns a string fully describing the runtime engine release information.
	 * 	The string returned is never null or empty.
	 */
	public String release() {
		return "2.0.00-rc.1";
	}


	/**
	 * Get the exact version of the Axil language supported by this API. The
	 * version information is in the form of: (major).(minor). For example,
	 * "1.0" or "1.1". The value returned is not the release information for
	 * the API, but the language version supported.
	 *
	 * @return
	 * 	Returns language version information. This string returned is a suitably
	 * 	stable format for an application to make compatibility decisions.
	 */
	public String language() {
		return "2.0";
	}


	/**
	 * Check that the thread safety and immutability constraints of the Axil
	 * configuration have not been violated.
	 */
	private void check() {
		if (sealed) {
			throw abort(
			    "An attempt has been made to reconfigure the Axil runtime " +
			    "engine after execution has been started. Configuration must " +
			    "be performed before any contexts are created or scripts " +
			    "executed.");
		}
	}


	/**
	 * Seal this configuration from further modification, preparing it for use in
	 * script execution. The object will throw an exception if any attempt is
	 * made to modify the object once it has been sealed.
	 */
	private void seal() {
		if (! sealed) {
			config.seal();
			sealed = true;
		}
	}


	protected AxilObject nil() {
		return Nil.object;
	}


	/**
	 * Get the default configuration. The configuration returned can be modified
	 * to alter the default, but will become immutable once put into use.
	 *
	 * @return
	 * 	Returns the default configuration. The default configuration will
	 * 	contain only the core module and the smallest possible memory and I/O
	 * 	settings as are practical to run modest-sized scripts.
	 */
	public Configuration configuration() {
		return config;
	}


	/**
	 * Add a source adapter for locating source files. By default, there is only
	 * a single adapter installed, one for reading the Axil standard library
	 * from the Axil distribution. Additional adapters can be installed, forming
	 * a chain. The adapters are consulted in the reverse order of installation.
	 * That is, a custom adapter will be referenced before the built-in adapter.
	 * <p/>
	 * This setup operation must be performed before the first attempt to
	 * configure or execute an Axil script. Any attempt to perform this setup
	 * activity afterwards will result in an exception.
	 *
	 * @param adapter
	 * 	The adapter to be installed.
	 */
	public void install(SourceAdapter adapter) {
		check();
		sources.add(require(adapter));
	}


	/**
	 * Manually set the accessor mechanisms for the given type. This mechanism
	 * is provided for manually setting up the accessibility when the annotation
	 * or interface-based mechanisms are not practical. Typically, this is only
	 * used when a type for which the source code is not available or cannot be
	 * altered needs to be accessible. This manual setup will completely override
	 * any interface or annotation discovery mechanism.
	 * <p/>
	 * All calls to this method must be performed before the first attempt is
	 * made to use a configuration or execute a script. Attempting this manual
	 * setup after Axil has been put into use will result in an exception.
	 *
	 * @param type
	 * 	The final, concrete class to be analyzed. An interface or abstract class
	 * 	cannot be specified. Any subclasses of the given class will not be
	 * 	recognized, only the exact given class.
	 *
	 * @param fields
	 * 	The set of fields to be made accessible. Only the fields explicitly
	 * 	specified will be allowed to be accessed.
	 *
	 * @see axil.api.access.AccessibleType
	 * @see axil.api.access.Accessible
	 */
	public void access(Class type, FieldDefinition... fields) {
		check();
		// TODO: Fix this
		throw unimplemented();
	}


	public List<SourceAdapter> sources() {
		return sources;
	}


	/**
	 * Set the case sensitivity of the Axil language and how any transformations
	 * are performed on Java fields and method names. For example:
	 * <pre>
	 *     Axil.instance().identifiers(Case.INSENSITIVE, Transformation.TITLE);
	 * </pre>
	 * This example causes the language to treat identifiers the same as long
	 * as they contain the same characters regardless of case. Any Java fields
	 * accessible to an Axil script are transformed into human-readable titles.
	 * <p/>
	 * This setup operation must be performed before the first attempt to
	 * configure or execute an Axil script. Any attempt to perform this setup
	 * activity afterwards will result in an exception.
	 *
	 * @param sensitivity
	 * 	This enumerated type controls the sensitivity of identifiers. This
	 * 	covers both values and function names. Note that the Axil standard
	 * 	library is coded to be case sensitive, so it functions correctly with
	 * 	either value. Axil is case sensitive by default.
	 *
	 * @param transform
	 * 	When performing field discovery on an accessible type, the Java-style
	 * 	fields and methods names are transformed into Axil identifiers. This
	 * 	enumerated type defines the rules that should be followed for the
	 * 	transformation.
	 */
	public void identifiers(Case sensitivity, Transformation transform) {
		check();
		this.sensitivity = require(sensitivity);
		this.transform = require(transform);
	}


	/**
	 * Get the case sensitivity for Axil identifiers. Axil is case sensitive
	 * by default.
	 *
	 * @return
	 * 	Returns the case sensitivity.
	 */
	public Case sensitivity() {
		return sensitivity;
	}


	/**
	 * Get the identifier transform currently in place. By default, Axil uses
	 * the DASHES transform, in keeping with the style of the Axil standard
	 * library.
	 *
	 * @return
	 * 	Returns the current transform.
	 */
	public Transformation transform() {
		return transform;
	}


	/**
	 * Return a formatting object based upon the given type and style
	 * specification string. The object returned may be a cached copy; this is
	 * harmless, since all Formatting objects are immutable and therefore
	 * thread-safe.
	 *
	 * @param type
	 * 	The Axil type to which the formatting object applies. The type given
	 * 	cannot be null.
	 *
	 * @param spec
	 * 	The specification for formatting. The specification format supported
	 * 	depends on the type. The string given cannot be null.
	 *
	 * @return
	 * 	Returns a formatting object, which is never null. Thew returned object
	 * 	can only be used with instances of the given type.
	 */
	public Formatting formatting(AxilType type, String spec) {
		if (type instanceof FormatBuilder) {
			return ((FormatBuilder)type).build(spec);
		}
		return new MaskFormatting(spec);
	}


	/**
	 * Get an Axil locale from the given code. The code is composed of an ISO
	 * 639-1 language code (e.g. "en"), an underscore, and and ISO 3166-1
	 * country code (e.g. "US").
	 *
	 * @param code
	 * 	An Axil locale code, such as "en_US". The string given cannot be null
	 * 	and must be exactly five characters in length.
	 *
	 * @return
	 * 	A non-null locale is returned. An exception is thrown if the locale is
	 * 	not defined or if the locale is not enabled. See the Axil.cfg
	 * 	configuration file for details.
	 */
	public Locale locale(String code) {
		return Locality.from(code);
	}


	/**
	 * Get the synthetic persona associated with the Axil runtime environment.
	 * That persona is synthesized at start-up from Java environmental defaults.
	 *
	 * @return
	 * 	Returns the system persona, which is never null.
	 */
	public Persona system() {
		return User.system();
	}


	/**
	 * Get the Axil type metadata for the type with the given unique identity.
	 *
	 * @param ident
	 * 	The unique identity of the type.
	 *
	 * @return
	 * 	Returns a non-null AxilType object. An exception is thrown if the type
	 * 	is not one of the known types. This can be the result of a typo in the
	 * 	identity or a missing use() in the configuration so that the type has
	 * 	been excluded.
	 */
	public AxilType type(String ident) {
		seal();
		return Repository.instance().type(ident);
	}


	/**
	 * Create an Argument object for the given name/value pair.
	 *
	 * @param name
	 * 	Any non-null string that contains no CR/LF characters nor a '.' (period)
	 * 	character. Axil allows spaces and punctuation in identifiers.
	 *
	 * @param value
	 * 	The value, which can be anything, including null.
	 *
	 * @return
	 * 	Returns the Argument object created.
	 */
	public Argument arg(String name, Object value) {
		return new Arg(name, value);
	}


	/**
	 * Create an Argument object with a null value. This is a convenience
	 * method provided for the compilation of scripts.
	 *
	 * @param name
	 * 	Any non-null string that contains no CR/LF characters nor a '.' (period)
	 * 	character. Axil allows spaces and punctuation in identifiers.
	 *
	 * @return
	 * 	Returns the Argument object created.
	 */
	public Argument arg(String name) {
		/*
		 * We use a placeholder as a non-null substitute for an actual host
		 * application object, only for compile time. If you are seeing this in
		 * your scripts, you are using a compile-time, not a run-time context.
		 */
		return new Arg(name, placeholder);
	}


	/**
	 * Create a new native, root context suitable for host application use. The
	 * context returned is mutable and has no parent.
	 *
	 * @param args
	 * 	A set of name/value pairs containing the host application objects
	 * 	available to the script.
	 *
	 * @return
	 * 	Returns a new context, which is never null.
	 */
	public Context context(Argument... args) {
		seal();
		return new AxilContext(null, args);
	}


	/**
	 * Create a new native context with the given object as the parent context.
	 *
	 * @param parent
	 * 	The parent context. The context given can be null, in which case a
	 * 	new root context is returned.
	 *
	 * @param args
	 * 	A set of name/value pairs containing the host application objects
	 * 	available to the script.
	 *
	 * @return
	 * 	Returns a new mutable context.
	 */
	public Context context(Context parent, Argument... args) {
		seal();
		return new AxilContext(parent, (Arg[])args);
	}


	/**
	 * Create a new execution environment.
	 *
	 * @param config
	 * 	The configuration for this environment. The configuration will be sealed
	 * 	at this point. The configuration given cannot be null.
	 *
	 * @param context
	 * 	The context containing the host application objects. The context given
	 * 	cannot be null.
	 *
	 * @param persona
	 * 	The persona associated with tis environment. A null persona may be given
	 * 	in which case the system persona will be used.
	 *
	 * @param output
	 * 	The output device where any console output will be written. A null
	 * 	object may be used to indicate that all output should be discarded.
	 *
	 * @return
	 * 	Returns a new execution environment. The object returned is immutable
	 * 	and can be reused; but, the output device for script output may be
	 * 	written to, so do not reuse across thread boundaries unless the output
	 * 	device is thread safe.
	 *
	 * @see axil.api.Axil#system()
	 */
	public Environment environment(Configuration config, Context context,
	                               Persona persona, Writer output) {
		seal();
		return new Env((Config)config, persona, context, output);
	}


	/**
	 * Compile the script provided by the given reader. The reader given is
	 * automatically closed either when the end of the source is reached or if
	 * there are any I/O or compilation exceptions.
	 *
	 * @param config
	 * 	The configuration for this compilation. The configuration will be sealed
	 * 	at this point. The configuration given cannot be null.
	 *
	 * @param context
	 * 	The context under which compilation will be performed. The get() method
	 * 	on the context will not be invoked, but the accessor() method will be.
	 *
	 * @param name
	 * 	The name of this script. This name will be reporting as the source
	 * 	element in any errors occurring in relation to the script.
	 *
	 * @param source
	 * 	A reader that can provide the textual source code for the script.
	 *
	 * @return
	 * 	Returns a compiled script suitable for execution. A null value is
	 * 	never returned.
	 *
	 * @throws axil.api.error.AxilCompilationException
	 * 	This exception is thrown if there are any compilation errors in the
	 * 	production of the script.
	 */
	public Script compile(Configuration config, Context context, String name,
	                      Reader source) throws AxilCompilationException {
		seal();
		Config cfg = (Config)config;
		AxilCompiler compiler = new AxilCompiler(name, source, false, cfg,
		        context);
		Executable exe = compiler.script();
		exe.name(empty(name) ? "(script)" : name);
		return exe;
	}
}
